﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace SharedGenomics.Domain
{
    public class DataColumnTypeResolver
    {
        #region Private Members

        private string _columnName;
        private ICollection<string> _values;
        private Dictionary<ColumnDataType, Func<ColumnDataType, ColumnDataType>> _transitionTable;

        #endregion

        #region Constructors

        public DataColumnTypeResolver(string columnName)
        {
            this.ColumnName = columnName;
            this.EncounteredDuplicate = false;
            this.EncounteredNull = false;

            this._values = new HashSet<string>();
            this._transitionTable = this.CreateTransitionTable();
        }

        #endregion

        #region Public Methods

        public void AddValue(string value)
        {
            string trimmedValue = value == null ? null : value.Trim();

            if (String.IsNullOrEmpty(trimmedValue))
            {
                this.EncounteredNull = true;
            }
            else
            {
                if (this._values.Contains(trimmedValue))
                    this.EncounteredDuplicate = true;

                this._values.Add(trimmedValue);

                //get the type for the added value and make the transition
                ColumnDataType t = this.GetDataType(trimmedValue);
                this.Transition(t);
            }
        }

        public DataColumnTypeBase CreateColumnType()
        {
            switch (this.CurrentColumnType)
            {
                case ColumnDataType.String:
                case ColumnDataType.Unknown:
                    Func<string, string> stringTransform = s => s;
                    DataColumnType<string> stringColumnType = new DataColumnType<string>(this.ColumnName, stringTransform, !this.EncounteredDuplicate, this.EncounteredNull);
                    stringColumnType.AddValues(this._values.Select(stringTransform));
                    return stringColumnType;

                case ColumnDataType.Int:
                    Func<string, int> intTransform = s => int.Parse(s);
                    DataColumnType<int> intColumnType = new DataColumnType<int>(this.ColumnName, s => int.Parse(s), !this.EncounteredDuplicate, this.EncounteredNull);
                    intColumnType.AddValues(this._values.Select(intTransform));
                    return intColumnType;

                case ColumnDataType.Double:
                    Func<string, double> doubleTransform = s => double.Parse(s);
                    DataColumnType<double> doubleColumnType = new DataColumnType<double>(this.ColumnName, s => double.Parse(s), !this.EncounteredDuplicate, this.EncounteredNull);
                    doubleColumnType.AddValues(this._values.Select(doubleTransform));
                    return doubleColumnType;

                case ColumnDataType.DateTime:
                    Func<string, DateTime> dtTransform = s => DateTime.Parse(s);
                    DataColumnType<DateTime> dtColumnType = new DataColumnType<DateTime>(this.ColumnName, s => DateTime.Parse(s), !this.EncounteredDuplicate, this.EncounteredNull);
                    dtColumnType.AddValues(this._values.Select(dtTransform));
                    return dtColumnType;

                default:
                    Debug.Fail("Unexpected ColumnDataType");
                    throw new InvalidOperationException("Unexpected ColumnDataType");
            }
        }

        #endregion

        #region Private Methods

        private ColumnDataType GetDataType(string value)
        {
            Debug.Assert(!String.IsNullOrEmpty(value));

            DateTime dt;
            int i;
            double d;

            if (DateTime.TryParse(value, out dt))
                return ColumnDataType.DateTime;
            else if (int.TryParse(value, out i))
                return ColumnDataType.Int;
            else if (double.TryParse(value, out d))
                return ColumnDataType.Double;
            else
                return ColumnDataType.String;
        }

        private void Transition(ColumnDataType newValueType)
        {
            this.CurrentColumnType = this._transitionTable[this.CurrentColumnType](newValueType);
        }

        private Dictionary<ColumnDataType, Func<ColumnDataType, ColumnDataType>> CreateTransitionTable()
        {
            return new Dictionary<ColumnDataType, Func<ColumnDataType, ColumnDataType>>
            {
                { ColumnDataType.Int, dt => (dt == ColumnDataType.Double || dt == ColumnDataType.Int)
                                                ? dt
                                                : ColumnDataType.String
                },
                { ColumnDataType.DateTime, dt => dt == ColumnDataType.DateTime ? ColumnDataType.DateTime : ColumnDataType.String },
                { ColumnDataType.Double, dt => (dt == ColumnDataType.Double || dt == ColumnDataType.Int)
                                                ? ColumnDataType.Double
                                                : ColumnDataType.String
                },
                { ColumnDataType.String, dt => ColumnDataType.String },
                { ColumnDataType.Unknown, dt => dt }
            };
        }

        #endregion

        #region Properties

        private string ColumnName
        {
            get { return this._columnName; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("ColumnName");

                this._columnName = value;
            }
        }

        private bool EncounteredNull { get; set; }
        private bool EncounteredDuplicate { get; set; }
        private ColumnDataType CurrentColumnType { get; set; }

        #endregion

        #region ColumnTypeName

        private enum ColumnDataType
        {
            Unknown = 0,
            DateTime,
            Int,
            Double,
            String
        }

        #endregion
    }
}
